查看原文
其他

粒子特效教程 | 多重GPU粒子力场

Unity Unity官方平台 2022-05-07

我们将分享加拿大游戏特效大神Mirza Beig的粒子特效的系列教程,该系列教程将帮助你了解如何使用粒子系统制作精美的特效。

 

往期教程回顾:


上一篇教程《GPU粒子力场》中,我们制作了一个自定义着色器,它能接收表示球体位移影响因子即力场的位置和半径,基于标准化偏移值来移动粒子顶点并给粒子着色。


本篇教程中,我们将用该着色器制作一个特别版本,它能通过使用数组来支持多个力场。下图的预览效果由本教程制作的效果和《创建3D均匀粒子网格》的均匀粒子网格结合而成。


Part 1:顶点着色器

创建上一篇教程中着色器文件的副本,然后修改文件名。

 

 

下面的代码中,只是在Field后添加了“s”,使Field一词变为复数形式。

Shader "Custom/Particles/GPU Force Fields Unlit (Tutorial)"


然后删除材质的半径和位置属性。我们不再需要这二个属性,因为我们会用C#脚本,将它们直接指定到着色器力场数组中。

Properties

{

    _MainTex("Texture", 2D) = "white" {}

 

    _ForceFieldRadius("Force Field Radius", Float) = 4.0

    _ForceFieldPosition("Force Field Position", Vector) = (0.0, 0.0, 0.0, 0.0)

 

    [HDR] _ColourA("Color A", Color) = (0.0, 0.0, 0.0, 0.0)

    [HDR] _ColourB("Color B", Color) = (1.0, 1.0, 1.0, 1.0)

}


我们还将删除了半径和位置的变量,将它们替换为力场数量和紧凑的力场数组。我们会用xyz保存每个力场的位置,用w保存半径。我们会用力场数量来结束迭代每个力场的循环,这样就不必处理整个数组。

 

本文中我们使用的数组大小为8,你可以按喜好使用更大的数值,例如:64。

sampler2D _MainTex;

float4 _MainTex_ST;

 

float _ForceFieldRadius;

float3 _ForceFieldPosition;

 

int _ForceFieldCount;

float4 _ForceFields[8];

 

float4 _ColourA;

float4 _ColourB;


拿一个数组举例,第一个力场的位置和半径会分别定义为_ForceField[0].xyz _ForceField[0].w。

 

现在我们可以编写用于计算和返回粒子偏移的函数。首先添加第二个参数“forceField”,因为我们没有表示力场半径和位置的全局变量,所以要在循环调用该函数的过程中,传入每个球体的信息。

float4 GetParticleOffset(float3 particleCenter, float4 forceField)


在函数顶部创建二个新变量,用于获取半径和位置。

float forceFieldRadius = forceField.w;

float3 forceFieldPosition = forceField.xyz;


这便是我们要修改的内容,现在可以将函数中的_ForceFieldRadius和_ForceFieldPosition替换为刚创建的二个新变量。在下面代码第12行的max函数中把0.0改为一个小数,以避免顶点在多重力场中消失。

 

其它代码保持不变,代码如下。

float4 GetParticleOffset(float3 particleCenter, float4 forceField)

{

    float forceFieldRadius = forceField.w;

    float3 forceFieldPosition = forceField.xyz;

     

    float distanceToParticle = distance(particleCenter, forceFieldPosition);

    float forceFieldRadiusAbs = abs(forceFieldRadius);

 

    float3 directionToParticle = normalize(particleCenter - forceFieldPosition);

 

    float distanceToForceFieldRadius = forceFieldRadiusAbs - distanceToParticle;

    distanceToForceFieldRadius = max(distanceToForceFieldRadius, 0.0001);

 

    distanceToForceFieldRadius *= sign(forceFieldRadius);

 

    float4 particleOffset;

 

    particleOffset.xyz = directionToParticle * distanceToForceFieldRadius;

    particleOffset.w = distanceToForceFieldRadius / (forceFieldRadius + 0.0001); //添加小数来避免被除数为0,以及在r=0.0时出现未定义的颜色或行为。

 

    return particleOffset;

}


修改偏移函数并实现数组后,为了完成多重力场的基本支持,我们只需要使用循环代码更新顶点着色器即可。

  

遍历整个数组长度比用变量力场数提早结束的速度更快。这是因为编译器默认会先尝试展开循环。这种情况下,我们必须在对应C#脚本中每帧创建一个新数组。


数组受限于力场数量,这样脚本会忽略不活动力场,或把这些力场的半径设为0,使它们不对任何粒子造成影响,这也是避免常量分配的较优解决方案。

v2f vert(appdata v)

{

    v2f o;

 

    float3 particleCenter = float3(v.tc0.zw, v.tc1.x);

     

    float3 vertexOffset = 0.0;

 

    for (int i = 0; i < _ForceFieldCount; i++)

    {

        vertexOffset += GetParticleOffset(particleCenter, _ForceFields[i]).xyz;

    }

 

    v.vertex.xyz += vertexOffset;

    o.vertex = UnityObjectToClipPos(v.vertex);

 

    // 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色

    o.color = v.color;

 

    o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);

 

    // 初始化tex coord变量

    o.tc0.zw = v.tc0.zw;

    o.tc1 = v.tc1;

 

    UNITY_TRANSFER_FOG(o,o.vertex);

    return o;

}


稍后我们将对顶点着色器进行一些修改,但现在我们先处理片段着色器。


Part 2:片段着色器

片段着色器所更新的代码行为第12-19行。由于多重力场会影响粒子的偏移,所以我们将从整个循环获取最大标准化偏移并使用它。

fixed4 frag(v2f i) : SV_Target

{

    // 采样纹理

    fixed4 col = tex2D(_MainTex, i.tc0);

 

    //让纹理颜色和粒子系统的顶点颜色输入相乘

    col *= i.color;

 

    float3 particleCenter = float3(i.tc0.zw, i.tc1.x);

 

    float maxNormalizedOffset = 0.0;

 

    for (int i = 0; i < _ForceFieldCount; i++)

    {

        maxNormalizedOffset = max(maxNormalizedOffset, GetParticleOffset(particleCenter, _ForceFields[i]).w);

    }

 

    col = lerp(col * _ColourA, col * _ColourB, maxNormalizedOffset);

 

    col *= col.a;

 

    // 应用模糊效果

    UNITY_APPLY_FOG(i.fogCoord, col);

    return col;

}


由于没有C#脚本填充力场数组,因此我们目前无法看到任何实际变化。


Part 3:GPU力场游戏对象

设置粒子系统的方法是把任何附加了该组件的游戏对象的任何子Transform都视为作力场对象。然后我们可以从父对象动态添加或移除Transform,来创建或销毁力场。

 

下面详解该脚本和上一片教程中单力场脚本之间的差异。

 

首先我们在第9行有一个常量内部变量,用来指定与着色器中的数组长度匹配的最大力场数量。

 

在第11行有Vector4类型的forceFields数组,我们会在Start()函数中将其初始化为最大长度,并指定为着色器中的等价变量。着色器数组此时没有初始化为它的长度,直到受到外部脚本的设置,所以这是我们立即执行此操作的原因。

 

每一帧我们都用着色器中的子对象数量更新力场数量,然后用循环来提取它们的位置和半径,这些信息会被指定到数组内当前迭代的力场向量中。当更新循环完成后,只要将数据复制到着色器数组即可。

 

最后,我们在OnDrawGizmos函数中循环处理数组,这样能可视化力场为球体。

 

下面是完整的C#脚本。

using System.Collections;

using System.Collections.Generic;

using UnityEngine;

 

[ExecuteInEditMode]

public class GPUParticleForceFields : MonoBehaviour

{

    public Material material;

    const int MAX_FORCE_FIELDS = 8; // 确保该数值匹配着色器全局数组大小。

 

    Vector4[] forceFields;

 

    void Start()

    {

        // 需要设置为支持的最大数组长度,因为它会决定着色器上的实际大小。

        forceFields = new Vector4[MAX_FORCE_FIELDS];

        material.SetVectorArray("_ForceFields", forceFields);

    }

 

    void LateUpdate()

    {

        material.SetInt("_ForceFieldCount", transform.childCount);

         

        for (int i = 0; i < transform.childCount; i++)

        {

            Transform childTransform = transform.GetChild(i);

 

            forceFields[i] = new Vector4(

 

                childTransform.position.x, childTransform.position.y, childTransform.position.z,

                childTransform.lossyScale.x / 2.0f);

        }

 

        material.SetVectorArray("_ForceFields", forceFields);

    }

 

    void OnDrawGizmos()

    {

        for (int i = 0; i < transform.childCount; i++)

        {

            Transform childTransform = transform.GetChild(i);

 

            float radius = childTransform.lossyScale.x / 2.0f;

            Gizmos.DrawWireSphere(childTransform.position, radius);

        }

    }

}


现在我们可以随意进行调整。


Part 4:均匀半径

我们将为着色器添加一个可选功能,以帮助缓解加法偏移混合的问题,使力场的混合效果更好,该功能适用于处理一些特别情况。

 

我们会通过静态开关来控制是否让着色器使用该功能,所以我们需要在材质属性中添加开关和均匀半径。

 

请注意,这里使用了范围滑块,因为这样在编辑器中更容易调整,你也可以使用常规的数值属性或较大的滑块范围。

Properties

{

    _MainTex("Texture", 2D) = "white" {}

 

    [Toggle(_USEUNIFORMRADIUS_ON)] _UseUniformRadius("Use Uniform Radius", Float) = 0.0

    _UniformRadius("Uniform Radius", Range(-10.0, 10.0)) = 1.0

 

    [HDR] _ColourA("Color A", Color) = (0.0, 0.0, 0.0, 0.0)

    [HDR] _ColourB("Color B", Color) = (1.0, 1.0, 1.0, 1.0)

}


我们需要定义开关的关键字。

CGPROGRAM

#pragma vertex vert

#pragma fragment frag

// 实现模糊效果

#pragma multi_compile_fog

 

#pragma shader_feature _USEUNIFORMRADIUS_ON

 

#include "UnityCG.cginc"


并且定义均匀半径变量。

sampler2D _MainTex;

float4 _MainTex_ST;

 

int _ForceFieldCount;

float4 _ForceFields[8];

 

float _UniformRadius;

 

float4 _ColourA;

float4 _ColourB;


我们处理混合的方法是应用均匀半径,然后使用所有力场中最小的偏移值。为了从函数获取最小偏移距离,我们将inout关键字和第一行参数一起使用,以便我们可以传入数值并修改原始数值。

 

该函数用于更新输入变量,具体方法是将输入变量与当前距离作对比,使它总是最小值。

float4 GetParticleOffset(float3 particleCenter, float4 forceField, inout float minDistanceToParticle)

{

    float forceFieldRadius;

    float3 forceFieldPosition = forceField.xyz;

 

    #ifdef _USEUNIFORMRADIUS_ON

        forceFieldRadius = _UniformRadius + 0.0001;

    #else      

        forceFieldRadius = forceField.w;

    #endif

 

    float distanceToParticle = distance(particleCenter, forceFieldPosition);

    float forceFieldRadiusAbs = abs(forceFieldRadius);             

 

    float3 directionToParticle = normalize(particleCenter - forceFieldPosition);

 

    float distanceToForceFieldRadius = forceFieldRadiusAbs - distanceToParticle;

    distanceToForceFieldRadius = max(distanceToForceFieldRadius, 0.0001);

 

    distanceToForceFieldRadius *= sign(forceFieldRadius);

     

    float4 particleOffset;

     

    particleOffset.xyz = directionToParticle * distanceToForceFieldRadius;

    particleOffset.w = distanceToForceFieldRadius / (forceFieldRadius + 0.0001); // Add small value to prevent divide by zero and undefined colour/behaviour at r = 0.0.

 

    minDistanceToParticle = min(minDistanceToParticle, distanceToParticle);

 

    return particleOffset;

}


我们可以修改顶点着色器部分,使脚本在均匀半径开关打开时,持续更新并使用最小距离。下面代码的红色行是改动的着色器代码。

 

我们将最小距离变量初始化为一个较大数值,这样后续迭代能保证返回较小数值。我们在代码中使用了99999.0。

v2f o;

 

float3 particleCenter = float3(v.tc0.zw, v.tc1.x);

 

float minDistanceToParticle = 99999.0;

 

float3 vertexOffset = 0.0;

 

for (int i = 0; i < _ForceFieldCount; i++)

{

    vertexOffset += GetParticleOffset(particleCenter, _ForceFields[i], minDistanceToParticle).xyz;

}

 

#ifdef _USEUNIFORMRADIUS_ON

 

    float3 normalizedVertexOffset = normalize(vertexOffset);

 

    float uniformRadiusAbs = abs(_UniformRadius);

    float minDistanceToUniformRadius = max(uniformRadiusAbs - minDistanceToParticle, 0.0);

 

    uniformRadiusAbs *= sign(_UniformRadius);

 

    vertexOffset = normalizedVertexOffset * minDistanceToUniformRadius;

 

#endif

 

v.vertex.xyz += vertexOffset;

o.vertex = UnityObjectToClipPos(v.vertex);


我们也需要对片段部分进行类似的改动。

col *= i.color;

 

float3 particleCenter = float3(i.tc0.zw, i.tc1.x);

 

float minDistanceToParticle = 99999.0;

float maxNormalizedOffset = 0.0;

 

for (int i = 0; i < _ForceFieldCount; i++)

{

    maxNormalizedOffset = max(maxNormalizedOffset, GetParticleOffset(particleCenter, _ForceFields[i], minDistanceToParticle).w);

}

 

col = lerp(col * _ColourA, col * _ColourB, maxNormalizedOffset);

 

col *= col.a;


着色器现在可以正常使用,我们可以在编辑器看到选项开关。


  

下面是Use Uniform Radius选项开启和关闭时的不同效果。

 

 

下面我们只需要更新C#组件,就可以生成并绘制均匀球体。

void OnDrawGizmos()

{

    bool useUniformRadius = material.GetFloat("_UseUniformRadius") == 1.0f ? true : false;

    float uniformRadius = material.GetFloat("_UniformRadius");

 

    for (int i = 0; i < transform.childCount; i++)

    {

        Transform childTransform = transform.GetChild(i);

 

        float radius = useUniformRadius ? uniformRadius : (childTransform.lossyScale.x / 2.0f);

        Gizmos.DrawWireSphere(childTransform.position, radius);

    }

}


这样就实现了我们想要的效果。


着色器代码

下面是完整的着色器代码。

Shader "Custom/Particles/GPU Force Fields Unlit (Tutorial)"

{

    Properties

    {

        _MainTex("Texture", 2D) = "white" {}

 

        [Toggle(_USEUNIFORMRADIUS_ON)] _UseUniformRadius("Use Uniform Radius", Float) = 0.0

        _UniformRadius("Uniform Radius", Range(-10.0, 10.0)) = 1.0

     

        [HDR] _ColourA("Color A", Color) = (0.0, 0.0, 0.0, 0.0)

        [HDR] _ColourB("Color B", Color) = (1.0, 1.0, 1.0, 1.0)

    }

 

    SubShader

    {

        Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }

        LOD 100

 

        Blend One One // 加法混合

        ZWrite Off //关闭深度测试

 

        Pass

        {

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            //实现模糊效果

            #pragma multi_compile_fog

 

            #pragma shader_feature _USEUNIFORMRADIUS_ON

 

            #include "UnityCG.cginc"

 

            struct appdata

            {

                float4 vertex : POSITION;

                fixed4 color : COLOR;

                float4 tc0 : TEXCOORD0;

                float4 tc1 : TEXCOORD1;

            };

 

            struct v2f

            {

                float4 tc0 : TEXCOORD0;

                float4 tc1 : TEXCOORD1;

                UNITY_FOG_COORDS(1)

                float4 vertex : SV_POSITION;

                fixed4 color : COLOR;

            };

 

            sampler2D _MainTex;

            float4 _MainTex_ST;

 

            int _ForceFieldCount;

            float4 _ForceFields[8];

 

            float _UniformRadius;

 

            float4 _ColourA;

            float4 _ColourB;

 

            float4 GetParticleOffset(float3 particleCenter, float4 forceField, inout float minDistanceToParticle)

            {

                float forceFieldRadius;

                float3 forceFieldPosition = forceField.xyz;

 

                #ifdef _USEUNIFORMRADIUS_ON

                    forceFieldRadius = _UniformRadius + 0.0001;

                #else      

                    forceFieldRadius = forceField.w;

                #endif

 

                float distanceToParticle = distance(particleCenter, forceFieldPosition);

                float forceFieldRadiusAbs = abs(forceFieldRadius);

 

                float3 directionToParticle = normalize(particleCenter - forceFieldPosition);

 

                float distanceToForceFieldRadius = forceFieldRadiusAbs - distanceToParticle;

                distanceToForceFieldRadius = max(distanceToForceFieldRadius, 0.0001);

 

                distanceToForceFieldRadius *= sign(forceFieldRadius);

 

                float4 particleOffset;

 

                particleOffset.xyz = directionToParticle * distanceToForceFieldRadius;

                particleOffset.w = distanceToForceFieldRadius / (forceFieldRadius + 0.0001); //添加小数来避免被除数为0,以及在r=0.0时出现未定义的颜色或行为。

 

                minDistanceToParticle = min(minDistanceToParticle, distanceToParticle);

 

                return particleOffset;

            }

 

            v2f vert(appdata v)

            {

                v2f o;

 

                float3 particleCenter = float3(v.tc0.zw, v.tc1.x);

 

                float minDistanceToParticle = 99999.0;

 

                float3 vertexOffset = 0.0;

 

                for (int i = 0; i < _ForceFieldCount; i++)

                {

                    vertexOffset += GetParticleOffset(particleCenter, _ForceFields[i], minDistanceToParticle).xyz;

                }

 

                #ifdef _USEUNIFORMRADIUS_ON

 

                    float3 normalizedVertexOffset = normalize(vertexOffset);

 

                    float uniformRadiusAbs = abs(_UniformRadius);

                    float minDistanceToUniformRadius = max(uniformRadiusAbs - minDistanceToParticle, 0.0);

 

                    uniformRadiusAbs *= sign(_UniformRadius);

 

                    vertexOffset = normalizedVertexOffset * minDistanceToUniformRadius;

 

                #endif

 

                v.vertex.xyz += vertexOffset;

                o.vertex = UnityObjectToClipPos(v.vertex);

 

                //从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色

                o.color = v.color;

 

                o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);

 

                // 初始化tex coord变量

                o.tc0.zw = v.tc0.zw;

                o.tc1 = v.tc1;

 

                UNITY_TRANSFER_FOG(o,o.vertex);

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target

            {

                // 采样纹理

                fixed4 col = tex2D(_MainTex, i.tc0);

 

                // 让纹理颜色和粒子系统的顶点颜色输入相乘

                col *= i.color;

 

                float3 particleCenter = float3(i.tc0.zw, i.tc1.x);

                 

                float minDistanceToParticle = 99999.0;

                float maxNormalizedOffset = 0.0;

 

                for (int i = 0; i < _ForceFieldCount; i++)

                {

                    maxNormalizedOffset = max(maxNormalizedOffset, GetParticleOffset(particleCenter, _ForceFields[i], minDistanceToParticle).w);

                }

 

                col = lerp(col * _ColourA, col * _ColourB, maxNormalizedOffset);

 

                col *= col.a;

 

                // 应用模糊效果

                UNITY_APPLY_FOG(i.fogCoord, col);

                return col;

            }

            ENDCG

        }

    }

}

小结

实现多重GPU粒子力场就介绍到这里,希望大家学以致用,牢牢掌握这些制作粒子特效的基础,从而制作出精美的特效。


更多Unity教程,尽在Unity官方中文论坛(UnityChina.cn)。


原文来源:mirzabeig.com


推荐阅读


官方活动

Unity趣味问答,赢取新年礼物 (最后一天)

2月16日18点前,参加Unity趣味问答,赢取限量Unity纪念礼物。了解详情,请点击此处。


Asset Store新春特惠

2月23日前,Asset Store资源商店将进行新春特惠活动,全场资源9折,更有顶级精品资源7折,赶紧选购吧。了解详情,请点击此处


Obstacle Tower挑战赛正式开启

Unity举办的Obstacle Tower挑战赛现在正式开启,此次比赛将为富有挑战的全新任务训练出最佳性能的代理。了解详情请点击此处


了解更多比赛流程,并开始训练自己的代理,请访问:

https://www.unity3d.com/OTC


Unite Shanghai 2019

5月10日-12日上海,Unite大会强势回归。技术门票正在热销中,购票即获指定Asset Store资源商店精品21款资源的5折优惠券。

购票请访问:Unite2019.csdn.net



点击“阅读原文”访问Unity官方中文论坛

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存